spb/autoscholar
Public
1import { bibtexFor, getPaperBySlug, plainCitationFor, risFor } from "@autoscholar/core";2import { parseBib } from "@autoscholar/qa";3import { publicArtifactKey } from "@autoscholar/storage";4import type { Metadata } from "next";5import Link from "next/link";6import { notFound } from "next/navigation";7import { CopyButton } from "@/components/CopyButton";8import { Disclosure } from "@/components/Disclosure";9import { PaperCard } from "@/components/PaperCard";10import { PdfViewer } from "@/components/PdfViewer";11import { StatusBadge } from "@/components/StatusBadge";12import { Tabs } from "@/components/Tabs";13import { disciplineLabel, fmtBytes, fmtDate, modeLabel, shortHash } from "@/lib/format";14import { t } from "@/lib/i18n";15import { db, storage } from "@/lib/server/db";1617export const dynamic = "force-dynamic";1819async function load(slug: string) {20 try {21 return await getPaperBySlug(db(), slug);22 } catch {23 return null;24 }25}2627export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {28 const { slug } = await params;29 const p = await load(slug);30 if (!p) return { title: "Paper not found" };31 const base = process.env.APP_BASE_URL ?? "http://localhost:8470";32 return {33 title: p.version.title,34 description: p.version.abstract.slice(0, 300),35 alternates: { canonical: `/research/${p.paper.slug}` },36 robots: p.paper.synthetic ? { index: false, follow: false } : undefined,37 openGraph: {38 title: p.version.title,39 description: p.version.abstract.slice(0, 300),40 type: "article",41 url: `${base}/research/${p.paper.slug}`,42 publishedTime: p.version.publishedAt.toISOString(),43 },44 other: {45 citation_title: p.version.title,46 citation_publication_date: p.version.publishedAt.toISOString().slice(0, 10),47 citation_technical_report_number: p.paper.publicId,48 citation_technical_report_institution: "AutoScholar.ai",49 citation_pdf_url: `${base}${p.version.pdfPublicPath}`,50 },51 };52}5354type ArtifactEntry = {55 path: string;56 kind: string;57 sha256: string;58 bytes: number;59 contentType: string;60 publicPath: string;61};62function Dl({ a }: { a: ArtifactEntry }) {63 return (64 <li className="flex items-center justify-between gap-3 py-1.5 text-sm">65 <a href={`${a.publicPath}?download=1`} className="font-mono text-xs break-all">66 {a.path}67 </a>68 <span className="subtle whitespace-nowrap text-xs">69 {fmtBytes(a.bytes)} · {shortHash(a.sha256, 8)}70 </span>71 </li>72 );73}7475export default async function PaperPage({ params }: { params: Promise<{ slug: string }> }) {76 const { slug } = await params;77 const p = await load(slug);78 if (!p) notFound();79 const base = process.env.APP_BASE_URL ?? "http://localhost:8470";80 const v = p.version;81 const meta = v.metadata as Record<string, unknown>;82 const withdrawn = meta.artifactsWithdrawn === true;83 const set = withdrawn ? [] : v.artifactSet;84 const url = `${base}/research/${p.paper.slug}`;85 const pub = new Date(v.publishedAt);86 const cite = {87 publicId: p.paper.publicId,88 title: v.title,89 year: pub.getUTCFullYear(),90 month: pub.getUTCMonth() + 1,91 versionNo: v.versionNo,92 url,93 keywords: v.keywords,94 abstract: v.abstract,95 };96 const figures = set.filter((a) => a.kind === "figure" && /\.(png|svg)$/i.test(a.path));97 const pdfFigures = set.filter((a) => a.kind === "figure" && /\.pdf$/i.test(a.path));98 const tex = set.filter((a) => a.kind === "manuscript" && /\.(tex|bib|cls|sty)$/i.test(a.path));99 const code = set.filter((a) => a.kind === "code");100 const data = set.filter((a) => a.kind === "data");101 const results = set.filter((a) => a.kind === "result" || a.kind === "table");102 const prov = (v.provenance ?? {}) as Record<string, unknown>;103 const qa = (v.qaSummary ?? {}) as {104 gates?: Array<{ gate: string; status: string; severity: string; message: string }>;105 warnings?: string[];106 policyVersion?: string;107 evaluatedAt?: string;108 };109 let bib: ReturnType<typeof parseBib> = [];110 const bibEntry = set.find((a) => a.path === "sources/references.bib");111 if (bibEntry) {112 try {113 bib = parseBib(114 (await storage().get(publicArtifactKey(p.paper.publicId, v.versionNo, bibEntry.path))).toString(115 "utf8",116 ),117 );118 } catch {119 bib = [];120 }121 }122 const jsonLd = {123 "@context": "https://schema.org",124 "@type": "ScholarlyArticle",125 headline: v.title,126 name: v.title,127 abstract: v.abstract,128 keywords: v.keywords.join(", "),129 datePublished: v.publishedAt.toISOString(),130 dateModified: p.paper.updatedAt.toISOString(),131 author: { "@type": "Organization", name: "AutoScholar (AI-generated research)", url: base },132 publisher: { "@type": "Organization", name: "AutoScholar.ai", url: base },133 identifier: p.paper.publicId,134 url,135 isAccessibleForFree: true,136 ...(set.length137 ? {138 encoding: {139 "@type": "MediaObject",140 contentUrl: `${base}${v.pdfPublicPath}`,141 encodingFormat: "application/pdf",142 },143 }144 : {}),145 about: disciplineLabel(p.paper.discipline),146 version: v.versionNo,147 ...(p.retraction ? { creativeWorkStatus: "Retracted" } : {}),148 };149 return (150 <article className="space-y-10">151 {/* biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD built from server data */}152 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />153 {p.retraction && (154 <div155 role="alert"156 className="rounded-md border p-4"157 style={{158 borderColor: "var(--color-danger-500)",159 background: "color-mix(in oklab, var(--color-danger-500) 8%, transparent)",160 }}161 >162 <p className="font-semibold">163 This working paper was retracted on {fmtDate(p.retraction.retractedAt)}.164 </p>165 <p className="mt-1 text-sm">{p.retraction.notice}</p>166 {withdrawn && (167 <p className="muted mt-1 text-sm">168 Its artifacts have been withdrawn from delivery. This page remains as a permanent record.169 </p>170 )}171 </div>172 )}173 <header className="grid gap-6 md:grid-cols-12">174 <div className="md:col-span-8">175 <div className="flex flex-wrap items-center gap-2 text-xs">176 <span className="font-mono">{p.paper.publicId}</span>177 <span className="subtle">·</span>178 <span>Version {v.versionNo}</span>179 <StatusBadge status={p.paper.status} />180 {p.paper.synthetic && <span className="badge">staging sample — not a real paper</span>}181 </div>182 <h1 className="serif mt-3 text-3xl font-semibold leading-tight sm:text-4xl">{v.title}</h1>183 <p className="mt-4 text-sm">184 <span className="font-medium">{t("byline")}</span>185 <span className="muted"> · Research computed on MacLustr</span>186 </p>187 <dl className="muted mt-4 grid grid-cols-2 gap-x-6 gap-y-1 text-sm sm:grid-cols-4">188 <div>189 <dt className="label">Released</dt>190 <dd>{fmtDate(p.paper.firstPublishedAt)}</dd>191 </div>192 <div>193 <dt className="label">Revised</dt>194 <dd>{fmtDate(v.publishedAt)}</dd>195 </div>196 <div>197 <dt className="label">Subject</dt>198 <dd>199 <Link href={`/research?discipline=${p.paper.discipline}`}>200 {disciplineLabel(p.paper.discipline)}201 </Link>202 </dd>203 </div>204 <div>205 <dt className="label">Methodology</dt>206 <dd>{modeLabel(p.paper.researchMode)}</dd>207 </div>208 </dl>209 <ul className="mt-4 flex flex-wrap gap-2" aria-label="Keywords">210 {v.keywords.map((k) => (211 <li key={k}>212 <Link href={`/research?q=${encodeURIComponent(k)}`} className="badge no-underline">213 {k}214 </Link>215 </li>216 ))}217 </ul>218 </div>219 <aside className="surface p-4 text-sm md:col-span-4">220 <p className="label mb-2">Downloads</p>221 {withdrawn ? (222 <p className="muted">Artifacts withdrawn.</p>223 ) : (224 <ul className="space-y-2">225 <li>226 <a className="btn btn-primary w-full" href={`${v.pdfPublicPath}?download=1`}>227 PDF228 </a>229 </li>230 {tex.length > 0 && (231 <li>232 <a233 className="btn w-full"234 href={`${set.find((a) => a.path === "manuscript/main.tex")?.publicPath ?? tex[0]!.publicPath}?download=1`}235 >236 LaTeX source237 </a>238 </li>239 )}240 {code.length > 0 && (241 <li>242 <a className="btn w-full" href="#code">243 Code ({code.length} files)244 </a>245 </li>246 )}247 {data.length > 0 && (248 <li>249 <a className="btn w-full" href="#data">250 Data ({data.length} files)251 </a>252 </li>253 )}254 </ul>255 )}256 <div className="mt-4">257 <Disclosure compact />258 </div>259 </aside>260 </header>261262 <section aria-labelledby="abstract" className="grid gap-8 md:grid-cols-12">263 <div className="md:col-span-8">264 <h2 id="abstract" className="serif text-xl font-semibold">265 Abstract266 </h2>267 <p className="prose-paper mt-3">{v.abstract}</p>268 {Array.isArray(meta.contributions) && (meta.contributions as string[]).length > 0 && (269 <>270 <h2 className="serif mt-8 text-xl font-semibold">Key contributions</h2>271 <ol className="prose-paper mt-3 list-decimal pl-5">272 {(meta.contributions as string[]).map((c) => (273 <li key={c}>{c}</li>274 ))}275 </ol>276 </>277 )}278 {typeof meta.methods === "string" && meta.methods && (279 <>280 <h2 className="serif mt-8 text-xl font-semibold">Methods</h2>281 <p className="prose-paper mt-3">{meta.methods}</p>282 </>283 )}284 {typeof meta.mainResults === "string" && meta.mainResults && (285 <>286 <h2 className="serif mt-8 text-xl font-semibold">Main results</h2>287 <p className="prose-paper mt-3">{meta.mainResults}</p>288 </>289 )}290 {typeof meta.limitations === "string" && meta.limitations && (291 <>292 <h2 className="serif mt-8 text-xl font-semibold">Limitations</h2>293 <p className="prose-paper mt-3">{meta.limitations}</p>294 </>295 )}296 </div>297 <div className="space-y-4 md:col-span-4">298 <div className="surface p-4 text-sm">299 <p className="label mb-2">Research status</p>300 <ul className="space-y-1">301 <li>AutoScholar working paper (preprint)</li>302 <li>Produced autonomously by an AI research workflow</li>303 <li>304 Automated critical review; <strong>not</strong> human peer review305 </li>306 <li>307 Deterministic quality gates:{" "}308 {qa.gates309 ? `${qa.gates.filter((g) => g.status === "pass").length}/${qa.gates.filter((g) => g.status !== "skip").length} passed`310 : "see provenance"}311 </li>312 <li>DOI: none (not registered)</li>313 </ul>314 </div>315 {p.summary.topics.length > 0 && (316 <div className="surface p-4 text-sm">317 <p className="label mb-2">Topics</p>318 <ul className="flex flex-wrap gap-2">319 {p.summary.topics.map((x) => (320 <li key={x.slug}>321 <Link href={`/topics/${x.slug}`} className="badge no-underline">322 {x.name}323 </Link>324 </li>325 ))}326 </ul>327 </div>328 )}329 </div>330 </section>331332 {!withdrawn && (333 <section aria-labelledby="pdf">334 <h2 id="pdf" className="serif text-xl font-semibold">335 Read the paper336 </h2>337 <div className="mt-3">338 <PdfViewer339 src={v.pdfPublicPath}340 title={`${p.paper.publicId} v${v.versionNo} — ${v.title}`}341 downloadHref={`${v.pdfPublicPath}?download=1`}342 />343 </div>344 </section>345 )}346347 {(figures.length > 0 || pdfFigures.length > 0) && (348 <section aria-labelledby="figures">349 <h2 id="figures" className="serif text-xl font-semibold">350 Figures351 </h2>352 <ul className="mt-3 grid gap-4 sm:grid-cols-2">353 {figures.map((f) => (354 <li key={f.path} className="surface p-3">355 {/* biome-ignore lint/performance/noImgElement: artifact images are served immutable from this origin */}356 <img357 src={f.publicPath}358 alt={f.path.split("/").pop() ?? "figure"}359 className="w-full"360 loading="lazy"361 />362 <p className="subtle mt-2 font-mono text-xs">{f.path.replace("results/figures/", "")}</p>363 </li>364 ))}365 {pdfFigures.map((f) => (366 <li key={f.path} className="surface p-3 text-sm">367 <a href={`${f.publicPath}?download=1`}>{f.path.replace("results/figures/", "")}</a>{" "}368 <span className="subtle">(vector PDF, {fmtBytes(f.bytes)})</span>369 </li>370 ))}371 </ul>372 </section>373 )}374375 {bib.length > 0 && (376 <section aria-labelledby="refs">377 <h2 id="refs" className="serif text-xl font-semibold">378 References ({bib.length})379 </h2>380 <p className="muted mt-1 text-xs">381 Every reference was verified against a scholarly registry before publication; identifiers link to382 the source of record.383 </p>384 <ol className="mt-3 space-y-2 text-sm">385 {bib.map((b) => {386 const f = b.fields;387 const href = f.doi388 ? `https://doi.org/${f.doi.replace(/^https?:\/\/doi\.org\//, "")}`389 : (f.url ?? (f.eprint ? `https://arxiv.org/abs/${f.eprint}` : undefined));390 return (391 <li key={b.key} id={`ref-${b.key}`} className="border-b pb-2 hairline">392 <span>{f.author ? `${f.author.replace(/\s+and\s+/g, ", ")}. ` : ""}</span>393 {f.year && <span>({f.year}). </span>}394 <span className="italic">{f.title?.replace(/[{}]/g, "")}</span>395 {f.journal && <span>. {f.journal}</span>}396 {f.booktitle && <span>. In {f.booktitle}</span>}397 {f.volume && <span>, {f.volume}</span>}398 {f.pages && <span>, {f.pages}</span>}.{" "}399 {href && (400 <a href={href} rel="noopener noreferrer" className="font-mono text-xs break-all">401 {f.doi ? `doi:${f.doi}` : href}402 </a>403 )}404 </li>405 );406 })}407 </ol>408 </section>409 )}410411 {(code.length > 0 || data.length > 0 || results.length > 0) && (412 <section aria-labelledby="repro" className="grid gap-6 md:grid-cols-3">413 <h2 id="repro" className="serif text-xl font-semibold md:col-span-3">414 Code, data and results415 </h2>416 {code.length > 0 && (417 <div id="code" className="surface p-4">418 <p className="label mb-1">Code</p>419 <ul>420 {code.map((a) => (421 <Dl key={a.path} a={a} />422 ))}423 </ul>424 </div>425 )}426 {data.length > 0 && (427 <div id="data" className="surface p-4">428 <p className="label mb-1">Data</p>429 <ul>430 {data.map((a) => (431 <Dl key={a.path} a={a} />432 ))}433 </ul>434 </div>435 )}436 {results.length > 0 && (437 <div className="surface p-4">438 <p className="label mb-1">Results and tables</p>439 <ul>440 {results.map((a) => (441 <Dl key={a.path} a={a} />442 ))}443 </ul>444 </div>445 )}446 </section>447 )}448449 <section aria-labelledby="reproducibility" className="grid gap-6 md:grid-cols-2">450 <div>451 <h2 id="reproducibility" className="serif text-xl font-semibold">452 Reproducibility453 </h2>454 <dl className="mt-3 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-sm">455 <dt className="label">Data availability</dt>456 <dd>{(meta.dataAvailability as string) || "Not applicable or not stated."}</dd>457 <dt className="label">Code availability</dt>458 <dd>{(meta.codeAvailability as string) || "Not applicable or not stated."}</dd>459 <dt className="label">Ethics</dt>460 <dd>461 {(meta.ethics as string) ||462 "No restricted activities; public data, mathematics or simulations only."}463 </dd>464 <dt className="label">Toolchain</dt>465 <dd className="font-mono text-xs">466 {prov.toolchain && Object.keys(prov.toolchain as object).length467 ? Object.entries(prov.toolchain as Record<string, string>)468 .map(([k, val]) => `${k} ${val}`)469 .join(" · ")470 : "not recorded"}471 </dd>472 <dt className="label">LaTeX source</dt>473 <dd>474 {tex.length475 ? "Released; the PDF was rebuilt from it in a clean environment during verification."476 : "Not released."}477 </dd>478 <dt className="label">Verified</dt>479 <dd>480 {qa.evaluatedAt ? `${fmtDate(qa.evaluatedAt)} under quality policy ${qa.policyVersion}` : "—"}481 </dd>482 </dl>483 {qa.warnings && qa.warnings.length > 0 && (484 <details className="mt-3 text-sm">485 <summary className="cursor-pointer">486 Non-blocking warnings recorded at verification ({qa.warnings.length})487 </summary>488 <ul className="muted mt-2 list-disc pl-5">489 {qa.warnings.map((w) => (490 <li key={w}>{w}</li>491 ))}492 </ul>493 </details>494 )}495 </div>496 <div>497 <h2 className="serif text-xl font-semibold">Provenance</h2>498 <dl className="mt-3 grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-sm">499 <dt className="label">Model</dt>500 <dd className="font-mono text-xs">{String(prov.model ?? "—")}</dd>501 <dt className="label">Reviewer model</dt>502 <dd className="font-mono text-xs">{String(prov.reviewerModel ?? "—")}</dd>503 <dt className="label">Instruction version</dt>504 <dd className="font-mono text-xs">505 v{String(prov.instructionVersion ?? "?")} · {shortHash(prov.instructionSha256 as string)}506 </dd>507 <dt className="label">Manifest digest</dt>508 <dd className="font-mono text-xs">{shortHash(prov.manifestSha256 as string)}</dd>509 <dt className="label">Execution</dt>510 <dd className="text-xs">511 {prov.startedAt512 ? `${fmtDate(prov.startedAt as string)} → ${prov.completedAt ? fmtDate(prov.completedAt as string) : "?"}`513 : "—"}{" "}514 · {String(prov.infrastructure ?? "MacLustr")}515 </dd>516 <dt className="label">Quality gates</dt>517 <dd>518 {qa.gates && qa.gates.length > 0 ? (519 <details className="text-xs">520 <summary className="cursor-pointer">{qa.gates.length} gates evaluated</summary>521 <ul className="mt-1 space-y-0.5">522 {qa.gates.map((g) => (523 <li key={g.gate}>524 <StatusBadge status={g.status} /> <span className="font-mono">{g.gate}</span>{" "}525 <span className="muted">— {g.message}</span>526 </li>527 ))}528 </ul>529 </details>530 ) : (531 "—"532 )}533 </dd>534 </dl>535 <p className="subtle mt-2 text-xs">536 Artifact digests (SHA-256) are listed with each download; the full list is in537 provenance/checksums.sha256.538 </p>539 </div>540 </section>541542 <section aria-labelledby="cite">543 <h2 id="cite" className="serif text-xl font-semibold">544 Cite this working paper545 </h2>546 <div className="mt-3">547 <Tabs548 tabs={[549 {550 label: "BibTeX",551 content: (552 <div>553 <pre className="surface overflow-auto p-4 text-xs">{bibtexFor(cite)}</pre>554 <div className="mt-2 flex gap-2">555 <CopyButton text={bibtexFor(cite)} />556 <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.bib`}>557 Download .bib558 </a>559 </div>560 </div>561 ),562 },563 {564 label: "RIS",565 content: (566 <div>567 <pre className="surface overflow-auto p-4 text-xs">{risFor(cite)}</pre>568 <div className="mt-2 flex gap-2">569 <CopyButton text={risFor(cite)} />570 <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.ris`}>571 Download .ris572 </a>573 </div>574 </div>575 ),576 },577 {578 label: "Plain text",579 content: (580 <div>581 <p className="surface p-4 text-sm">{plainCitationFor(cite)}</p>582 <div className="mt-2 flex gap-2">583 <CopyButton text={plainCitationFor(cite)} />584 <a className="btn btn-sm" href={`/research/${p.paper.slug}/cite.txt`}>585 Download .txt586 </a>587 </div>588 </div>589 ),590 },591 ]}592 />593 </div>594 </section>595596 <section aria-labelledby="history" className="grid gap-6 md:grid-cols-2">597 <div>598 <h2 id="history" className="serif text-xl font-semibold">599 Version history600 </h2>601 <ol className="mt-3 space-y-2 text-sm">602 {p.versions.map((x) => (603 <li key={x.id} className="surface flex items-start justify-between gap-3 p-3">604 <div>605 <span className="font-medium">Version {x.versionNo}</span>{" "}606 <span className="muted">· {fmtDate(x.publishedAt)}</span>607 <p className="muted mt-1">{x.changelog}</p>608 </div>609 {!withdrawn && (610 <a className="btn btn-sm" href={`${x.pdfPublicPath}?download=1`}>611 PDF v{x.versionNo}612 </a>613 )}614 </li>615 ))}616 </ol>617 </div>618 <div>619 <h2 className="serif text-xl font-semibold">Corrections and notices</h2>620 {p.corrections.length === 0 && !p.retraction ? (621 <p className="muted mt-3 text-sm">No corrections have been issued.</p>622 ) : (623 <ul className="mt-3 space-y-2 text-sm">624 {p.corrections.map((c) => (625 <li key={c.id} className="surface p-3">626 <span className="font-medium">627 Correction v{c.fromVersionNo} → v{c.toVersionNo}628 </span>{" "}629 <span className="muted">· {fmtDate(c.createdAt)}</span>630 <p className="mt-1">{c.notice}</p>631 </li>632 ))}633 {p.retraction && (634 <li className="surface p-3">635 <span className="font-medium">Retraction</span>{" "}636 <span className="muted">· {fmtDate(p.retraction.retractedAt)}</span>637 <p className="mt-1">{p.retraction.notice}</p>638 </li>639 )}640 </ul>641 )}642 </div>643 </section>644645 {p.related.length > 0 && (646 <section aria-labelledby="related">647 <h2 id="related" className="serif text-xl font-semibold">648 Related working papers649 </h2>650 <p className="muted mt-1 text-xs">651 Selected by shared discipline or keywords recorded in verified metadata.652 </p>653 <ul className="mt-3 grid gap-4 md:grid-cols-2">654 {p.related.map((r) => (655 <li key={r.publicId}>656 <PaperCard p={r} />657 </li>658 ))}659 </ul>660 </section>661 )}662 <Disclosure />663 </article>664 );665}666